package com.xuan.judge.service.impl;


import cn.hutool.core.date.StopWatch;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ArrayUtil;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.model.Bind;
import com.github.dockerjava.api.model.HostConfig;
import com.github.dockerjava.api.model.Image;
import com.github.dockerjava.api.model.Volume;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.netty.NettyDockerCmdExecFactory;
import com.xuan.common.core.constants.Constants;
import com.xuan.common.core.constants.JudgeConstants;
import com.xuan.common.core.enums.CodeRunStatus;
import com.xuan.judge.callback.DockerStartResultCallback;
import com.xuan.judge.callback.StatisticsCallback;
import com.xuan.judge.domain.CompileResult;
import com.xuan.judge.domain.SandBoxExecResult;
import com.xuan.judge.service.SandBoxService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class SandBoxServiceImpl implements SandBoxService {

    private String userCodeDir;

    private String userCodeFileName;

    private DockerClient dockerClient;

    private String containerId;

    @Value("${sandbox.docker.host:tcp://localhost:2375}")
    private String dockerHost;

    @Value("${sandbox.limit.memoryLimit:100000000}")
    private Long memoryLimit;

    @Value("${sandbox.limit.memorySwapLimit:100000000}")
    private Long memorySwapLimit;

    @Value("${sandbox.limit.cpuLimit:1}")
    private Long cpuLimit;

    @Value("${sandbox.limit.time:5}")
    private Long maxLimitTime;


    @Override
    public SandBoxExecResult execJavaCode(Long userId, String execCode, List<String> inputList) {
        // 1. 将要执行的代码存放到本地文件,以便后续将文件挂载到 docker 容器中
        createUserCodeFile(userId, execCode);
        // 2. 创建 docker 容器, 将本机文件挂载到 docker 目录中
        initDockerSandBox();
        // 3. 编译
        CompileResult compileResult = compileCodeByDocker();
        if (!compileResult.getCompile()) {
            // 在返回结果前删除文件和对应的容器
            deleteCodeFile();
            deleteContainer();
            return SandBoxExecResult.fail(CodeRunStatus.COMPILE_FAIL, compileResult.getMessage());
        }
        // 4. 执行代码
        return execJavaCodeByDocker(inputList);
    }


    /**
     *  执行创建 CMD 指令的操作,并返回该指令的 id
     * @param inputArgs     题目方法的参数, 多个参数之间用 ' ' 隔开
     * @param javaCmdArr    java 操作基本指令 + 输入参数
     * @param containerId   容器id
     * @return              该执行指令的 id
     */
    private String execCreateCmd(String inputArgs, String[] javaCmdArr, String containerId) {
        if (StringUtils.isNotBlank(inputArgs)) {
            String[] inputArr = inputArgs.split(" ");   // 方法入参
            javaCmdArr = ArrayUtil.append(javaCmdArr, inputArr);
        }
        ExecCreateCmdResponse cmdResponse = dockerClient.execCreateCmd(containerId)
                .withCmd(javaCmdArr)
                .withAttachStdout(true)
                .withAttachStderr(true)
                .exec();

        return cmdResponse.getId();
    }

    /**
     *  创建本地文件,存放用户要执行的代码
     * @param userId
     * @param userCode
     */
    private void createUserCodeFile(Long userId, String userCode) {
        String questionCodeDir = System.getProperty("user.dir") + File.separator + JudgeConstants.QUESTION_CODE_DIR;
        if (!FileUtil.exist(questionCodeDir)) {
            FileUtil.mkdir(questionCodeDir);
        }
        // 根据 userId+时间 生成一个目录,在该目录下存放 Solution.java文件
        String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
        userCodeDir = questionCodeDir + File.separator + userId + JudgeConstants.UNDERLINE_SEPARATOR + dateTime;
        userCodeFileName = userCodeDir + File.separator + JudgeConstants.EXEC_JAVA_CLASS_NAME;
        FileUtil.writeString(userCode, userCodeFileName, Constants.UTF8);
    }

    /**
     *  初始化 docker 沙箱
     *  包括 docker客户端的连接, docker 镜像的拉取和 docker 容器的创建
     */
    private void initDockerSandBox() {
        DefaultDockerClientConfig clientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder()
                .withDockerHost(dockerHost)       // 配置 docker 服务地址，以便连接 docker
                .build();
        dockerClient = DockerClientBuilder.getInstance(clientConfig)
                .withDockerCmdExecFactory(new NettyDockerCmdExecFactory())
                .build();
        // 使用 dockerClient 拉取 docker 镜像
        pullDockerImage();
        // 创建 docker 容器, 限制资源, 控制权限
        CreateContainerCmd createContainerCmd = dockerClient.createContainerCmd(JudgeConstants.JAVA_ENV_IMAGE)
                .withName(JudgeConstants.JAVA_JUDGE_CONTAIN_NAME);
        HostConfig hostConfig = getHostConfig();
        CreateContainerResponse containerResponse = createContainerCmd.withHostConfig(hostConfig)
                .withAttachStdout(true)         // 配置标准输出:
                .withAttachStderr(true)         // 标准错误输出:在命令执行出错时获取命令行的错误提示
                .withTty(true)                  // 为容器配置一个虚终端
                .exec();
        // 记录容器 ID
        containerId = containerResponse.getId();
        // 启动容器
        dockerClient.startContainerCmd(containerId).exec();
    }


    /**
     *  拉取 docker 镜像,若不存在
     */
    private void pullDockerImage() {
        // 1. 先判断要拉取的镜像是否存在
        ListImagesCmd listImagesCmd = dockerClient.listImagesCmd();
        List<Image> imageList = listImagesCmd.exec();
        for (Image image : imageList) {
            String[] repoTags = image.getRepoTags();
            if (repoTags != null && repoTags.length > 0 && JudgeConstants.JAVA_ENV_IMAGE.equals(repoTags[0])) {
                return;
            }
        }
        // 2. 不存在则拉取
        PullImageCmd pullImageCmd = dockerClient.pullImageCmd(JudgeConstants.JAVA_ENV_IMAGE);
        try {
            pullImageCmd.exec(new PullImageResultCallback()).awaitCompletion();     // 阻塞等待镜像拉取完成
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     *  对容器相关参数的配置
     *  比如: 对资源,网络访问和文件读写权限的限制
     * @return
     */
    private HostConfig getHostConfig() {
        HostConfig hostConfig = new HostConfig();
        // 指的文件的挂载目录
        hostConfig.setBinds(new Bind(userCodeDir, new Volume(JudgeConstants.DOCKER_USER_CODE_DIR)));
        hostConfig
                .withMemory(memoryLimit)            // 分配给容器的内存
                .withMemorySwap(memorySwapLimit)    // 内存交换区
                .withCpuCount(cpuLimit)             // CPU 数量
                .withNetworkMode("none")            // 禁用网络
                .withReadonlyRootfs(true);          // 限制文件只读权限
        return hostConfig;
    }


    /**
     *  编译代码
     * @return
     */
    private CompileResult compileCodeByDocker() {
        // 获取 docker 编译指令id
        String cmdId = execCreateCmd(null, JudgeConstants.DOCKER_JAVA_FILE_COMPILE_CMD, containerId);
        DockerStartResultCallback resultCallBack  = new DockerStartResultCallback();
        CompileResult compileResult = new CompileResult();
        try {
            dockerClient.execStartCmd(cmdId).
                    exec(resultCallBack)
                    .awaitCompletion();
            if (CodeRunStatus.FAILED.equals(resultCallBack.getCodeRunStatus())) {
                compileResult.setCompile(false);
                compileResult.setMessage(resultCallBack.getErrorMessage());
            } else {
                compileResult.setCompile(true);
            }
            return compileResult;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     *  执行编辑后的 Java 代码
     * @param inputList
     * @return
     */
    private SandBoxExecResult execJavaCodeByDocker(List<String> inputList) {
        List<String> execOutputList = new ArrayList<>();        // 记录代码的执行结果
        long maxMemory = 0;                                     // 记录所有输入示例中最大的运行内存
        long maxTime = 0;                                       // 记录所有输入示例中最大的运行时间
        // 执行每个测试用例
        for (String input : inputList) {
            String cmdId = execCreateCmd(input, JudgeConstants.DOCKER_EXEC_JAVA_CLASS_CMD, containerId);
            // 在代码开始执行前,开启监控
            StatisticsCallback statisticsCallback = new StatisticsCallback();
            StatsCmd statsCmd = dockerClient.statsCmd(containerId);
            statsCmd.exec(statisticsCallback);      // statisticsCallback 是回调对象
            // 在代码执行前开始计时
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();

            // 开始执行代码并限定最大运行时间,防止用户写出死循环,导致线程不能正常终止
            DockerStartResultCallback resultCallback = new DockerStartResultCallback();
            try {
                dockerClient.execStartCmd(cmdId)
                        .exec(resultCallback)
                        .awaitCompletion(maxLimitTime, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            stopWatch.stop();       // 结束计时
            statsCmd.close();       // 结束docker容器执行统计
            if (CodeRunStatus.FAILED.equals(resultCallback.getCodeRunStatus())) {
                return SandBoxExecResult.fail(CodeRunStatus.NOT_ALL_PASSED, resultCallback.getErrorMessage());
            }
            // 统计最大运行时内存,时间,输出结果
            if (statisticsCallback.getMaxUseMemory() != null) {
                maxMemory = Math.max(maxMemory, statisticsCallback.getMaxUseMemory());
            }
            maxTime = Math.max(maxTime, stopWatch.getTotalTimeMillis());
            execOutputList.add(resultCallback.getMessage());
        }
        // 删除容器
        // 删除用户代码对应的文件
        deleteContainer();
        deleteCodeFile();
        return SandBoxExecResult.success(execOutputList, maxTime, maxMemory);
    }


    private void deleteCodeFile() {
        FileUtil.del(userCodeDir);
    }

    private void deleteContainer() {
        // 执行完成后删除容器
        dockerClient.stopContainerCmd(containerId).exec();
        dockerClient.removeContainerCmd(containerId).exec();
        // 断开与 docker 的连接
        try {
            dockerClient.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
